/**
 * Copyright (C) 2010 Cloudfarming <info@cloudfarming.nl>
 *
 * Licensed under the Eclipse Public License - v 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.cloudfarming.client.model;

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.swing.event.UndoableEditEvent;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import org.openide.awt.UndoRedo;
import org.openide.util.Exceptions;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;

/**
 *
 * Base class with bean support
 * 
 * Bean implements Transferable for drag and drop support.
 * Transferable methods are implemented with defaults, sub classes should
 * override when needed.
 * 
 * @author Timon Veenstra
 */
@MappedSuperclass
public abstract class Bean implements Transferable{
    
    public static final String PROP_NAME = "name";
    public static final DataFlavor BEAN_FLAVOR = new DataFlavor();

    @Column(name = "NAME")
    private String name;
    @Transient
    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
    @Transient
    private List<UndoRedo.Manager> undoRedoManagers = new ArrayList<UndoRedo.Manager>();

    public enum BeanState {

        NORMAL, UNDO
    };
    @Transient
    public BeanState currentState = BeanState.NORMAL;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        String old = this.name;
        this.name = name;
        getPropertyChangeSupport().firePropertyChange(PROP_NAME, old, name);
        fireUndoableEvent(PROP_NAME, this, old, name);
    }

    private BeanState getCurrentState() {
        return currentState;
    }

    private void setCurrentState(BeanState currentState) {
        this.currentState = currentState;
    }

    public void addPropertyChangeListener(PropertyChangeListener pcl) {
        propertyChangeSupport.addPropertyChangeListener(pcl);
    }

    public void addPropertyChangeListener(String propertyName, PropertyChangeListener pcl) {
        propertyChangeSupport.addPropertyChangeListener(propertyName, pcl);
    }

    public void removePropertyChangeListener(PropertyChangeListener pcl) {
        propertyChangeSupport.removePropertyChangeListener(pcl);
    }

    public void removePropertyChangeListener(String propertyName, PropertyChangeListener pcl) {
        propertyChangeSupport.removePropertyChangeListener(propertyName, pcl);
    }

    public void addUndoableEditListener(UndoRedo.Manager manager) {
        undoRedoManagers.add(manager);
    }

    public void removeUndoableEditListener(UndoRedo.Manager manager) {
        undoRedoManagers.remove(manager);
    }

    protected PropertyChangeSupport getPropertyChangeSupport() {
        return propertyChangeSupport;
    }

    public void fireUndoableEvent(String property, Bean source, Object oldValue, Object newValue) {
        if (getCurrentState() != BeanState.UNDO) {
            for (UndoRedo.Manager manager : undoRedoManagers) {
                ReUndoableEdit reUndoableEdit = new ReUndoableEdit(property, source, oldValue, newValue);
                UndoableEditEvent undoableEditEvent = new UndoableEditEvent(this, reUndoableEdit);

                assert manager != null;
                assert undoableEditEvent != null;


                manager.undoableEditHappened(undoableEditEvent);
            }
        }
    }

    @Override
    public DataFlavor[] getTransferDataFlavors() {
        return new DataFlavor[]{BEAN_FLAVOR};
    }

    @Override
    public boolean isDataFlavorSupported(DataFlavor flavor) {
        return BEAN_FLAVOR.equals(flavor);
    }

    @Override
    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
        return this;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Bean other = (Bean) obj;
        if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
            return false;
        }
        if (this.propertyChangeSupport != other.propertyChangeSupport && (this.propertyChangeSupport == null || !this.propertyChangeSupport.equals(other.propertyChangeSupport))) {
            return false;
        }
        if (this.undoRedoManagers != other.undoRedoManagers && (this.undoRedoManagers == null || !this.undoRedoManagers.equals(other.undoRedoManagers))) {
            return false;
        }
        if (this.currentState != other.currentState) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 47 * hash + (this.name != null ? this.name.hashCode() : 0);
        hash = 47 * hash + (this.propertyChangeSupport != null ? this.propertyChangeSupport.hashCode() : 0);
        hash = 47 * hash + (this.undoRedoManagers != null ? this.undoRedoManagers.hashCode() : 0);
        hash = 47 * hash + (this.currentState != null ? this.currentState.hashCode() : 0);
        return hash;
    }

    /*
     * Custom UndoableEdit extending abstractUndoableEdit
     * Custom implemented Undo & Redo functionality
     * Will call setters on bean using reflection when undo or redo is called.
     *
     *
     * @Author Gerben Feenstra & Merijn Zengers
     *
     */
    private class ReUndoableEdit extends AbstractUndoableEdit {

        private Object oldValue;
        private Object newValue;
        private Bean source;
        private String property;

        public ReUndoableEdit(String property, Bean source, Object oldValue, Object newValue) {
            super();
            this.oldValue = oldValue;
            this.newValue = newValue;
            this.source = source;
            this.property = property;
        }

        private Method getMethod() {
            try {
                return source.getClass().getMethod("set" + property.replaceFirst(property.substring(0, 1), property.substring(0, 1).toUpperCase()), newValue.getClass());
            } catch (NoSuchMethodException ex) {
                Exceptions.printStackTrace(ex);
            } catch (SecurityException ex) {
                Exceptions.printStackTrace(ex);
            }
            return null;
        }

        @Override
        public void undo() throws CannotUndoException {
            try {
                super.undo();
                Method method = getMethod();
                setCurrentState(BeanState.UNDO);
                method.invoke(source, oldValue);
                setCurrentState(BeanState.NORMAL);
            } catch (SecurityException ex) {
                Exceptions.printStackTrace(ex);
            } catch (IllegalAccessException ex) {
                Exceptions.printStackTrace(ex);
            } catch (IllegalArgumentException ex) {
                Exceptions.printStackTrace(ex);
            } catch (InvocationTargetException ex) {
                Exceptions.printStackTrace(ex);
            }
        }

        @Override
        public void redo() throws CannotRedoException {
            try {
                super.redo();
                Method method = getMethod();
                setCurrentState(BeanState.UNDO);
                method.invoke(source, newValue);
                setCurrentState(BeanState.NORMAL);
            } catch (IllegalAccessException ex) {
                Exceptions.printStackTrace(ex);
            } catch (IllegalArgumentException ex) {
                Exceptions.printStackTrace(ex);
            } catch (InvocationTargetException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }
}
